package com.simpou.commons.persistence.jpa.dao.impl;

import com.simpou.commons.model.entity.BaseEntity;
import com.simpou.commons.model.entity.IdentifiableEntity;
import com.simpou.commons.persistence.common.Cache;
import com.simpou.commons.persistence.common.Transaction;
import com.simpou.commons.persistence.dao.ObjectDAO;
import com.simpou.commons.persistence.jpa.common.JpaCache;
import com.simpou.commons.persistence.jpa.common.JpaTransaction;
import com.simpou.commons.utils.pagination.PageLimits;
import com.simpou.commons.utils.validation.Assertions;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.List;


/**
 * Gerenciador de entidades. Classe padrão para acesso e manipulação de
 * entidades persistidas.
 *
 * @author Jonas Pereira
 * @version 2013-06-01
 * @since 2012-09-29
 */
public abstract class AbstractJpaDAOImpl implements ObjectDAO {
    @Override
    public <T extends BaseEntity> T create(final T entity)
            throws Exception {
        getEntityManager().persist(Assertions.valid(entity));

        return entity;
    }

    @Override
    public void refresh(final BaseEntity entity) throws Exception {
        try{
            getEntityManager().refresh(entity);
        }catch (Exception ex){
            //estes erros são aleatórios e não são relevantes
        }
    }

    @Override
    public <T extends BaseEntity> T update(final T entity)
            throws Exception {
        return getEntityManager().merge(Assertions.valid(entity));
    }

    @Override
    public void delete(final BaseEntity entity) throws Exception {
        refresh(Assertions.notNull(entity));
        final EntityManager em = getEntityManager();
        em.remove(em.merge(entity));
    }

    @Override
    public <T extends IdentifiableEntity<?>> void delete(final Class<T> clasz,
                                                         final Serializable id) throws Exception {
        Assertions.notNull(id);

        final T single = getSingle(clasz, id);

        if (single == null) {
            throw new EntityNotFoundException();
        } else {
            delete(single);
        }
    }

    @Override
    public <T extends BaseEntity> void deleteAll(final Class<T> clasz)
            throws Exception {
        execute("DELETE FROM " + clasz.getSimpleName());
    }

    @Override
    public <T extends IdentifiableEntity<?>> T getSingle(final Class<T> clasz,
                                                         final Serializable id) throws Exception {
        Assertions.notNull(id);

        final EntityManager em = getEntityManager();
        final T t = em.find(clasz, id);

        if (t != null) {
            refresh(t);
        }

        return t;
    }

    private <T extends BaseEntity> List<T> refreshList(final List<T> list) throws Exception {
        for (T entity : list) {
            refresh(entity);
        }
        return list;
    }

    @Override
    public <T extends BaseEntity> List<T> getList(final Class<T> clasz)
            throws Exception {
        return refreshList(getQuery(clasz, clasz, null, false).getResultList());
    }

    @Override
    public <T extends BaseEntity> Long count(final Class<T> clasz)
            throws Exception {
        return getQuery(clasz, Long.class, null, true).getSingleResult();
    }

    @Override
    public <T extends BaseEntity> List<T> getList(final Class<T> clasz,
                                                  final PageLimits limits) throws Exception {
        return refreshList(getQuery(clasz, clasz, limits, false).getResultList());
    }

    @Override
    public <T extends BaseEntity> List<T> getNamedList(final Class<T> clasz,
                                                       final String namedQuery, final PageLimits limits, final Object... params)
            throws Exception {
        return refreshList(getNamedQuery(namedQuery, clasz, limits, params).getResultList());
    }

    @Override
    public <T extends BaseEntity> T getNamedSingle(final Class<T> clasz,
                                                   final String namedQuery, final Object... params)
            throws Exception {
        final TypedQuery<T> typedQuery = getNamedQuery(namedQuery, clasz, null,
                params);

        return getSingle(clasz, typedQuery);
    }

    @Override
    public int execute(final String stringQuery, final Object... params)
            throws Exception {
        Assertions.notEmpty(stringQuery, "Query can not be empty.");

        return getQuery(null, stringQuery, null, params).executeUpdate();
    }

    @Override
    public int namedExecute(final String namedQuery, final Object... params)
            throws Exception {
        Assertions.notEmpty(namedQuery, "A name expected.");

        return getNamedQuery(namedQuery, null, null, params).executeUpdate();
    }

    @Override
    public <T> T getSingle(final Class<T> objClass,
                           final String stringQuery, final Object... params)
            throws Exception {
        Assertions.notEmpty(stringQuery, "Query can not be empty.");

        final TypedQuery<T> query = getQuery(objClass, stringQuery, null, params);

        return getSingle(objClass, query);
    }

    @Override
    public Long count(final String stringQuery, final Object... params)
            throws Exception {
        Assertions.notEmpty(stringQuery, "Query can not be empty.");

        return getQuery(Long.class, stringQuery, null, params).getSingleResult();
    }

    @Override
    public <T> List<T> getList(final Class<T> objClass,
                               final String stringQuery, final PageLimits limits,
                               final Object... params) throws Exception {
        Assertions.notEmpty(stringQuery, "Query can not be empty.");

        final List<T> list = getQuery(objClass, stringQuery, limits, params).getResultList();
        if(BaseEntity.class.isAssignableFrom(objClass)){
            refreshList((List<? extends BaseEntity>)list);
        }
        return list;
    }

    /**
     * Evita lançamento de "NoResultException" caso query não gerar o objeto
     * desejado. Certifica que o objeto retornado é o mais recente.
     *
     * @param objClass Classe do objeto.
     * @param query    Query.
     * @return Objeto mais recente ou null se query não retornar resultado.
     * @throws Exception Se houver.
     */
    public <T> T getSingle(final Class<T> objClass, final TypedQuery<T> query)
            throws Exception {
        T obj;

        try {
            obj = query.getSingleResult();

            if (BaseEntity.class.isAssignableFrom(objClass)) {
                // realiza refresh somente em entidades gerenciadas
                refresh((BaseEntity)obj);
            }
        } catch (final NoResultException e) {
            obj = null;
        }

        return obj;
    }

    /**
     * <p>getEntityManager.</p>
     *
     * @return a {@link javax.persistence.EntityManager} object.
     */
    protected abstract EntityManager getEntityManager();

    /**
     * Monta query.
     *
     * @param clasz       Classe dos objetos de retorno.
     * @param stringQuery Query JPQL.
     * @param limits      Limites.
     * @param params      Parâmetros da query.
     * @return Query.
     */
    protected <T> TypedQuery<T> getQuery(final Class<T> clasz,
                                         final String stringQuery, final PageLimits limits,
                                         final Object... params) {
        final TypedQuery<T> query = getEntityManager().createQuery(stringQuery, clasz);

        return getQuery(query, limits, params);
    }

    /**
     * Não suporta contagem.
     *
     * @param resultClass Classe dos objetos de retorno.
     * @param limits      Limites.
     * @param namedQuery  Nome da "Named query" anotada na entidade.
     * @param params      Parâmetros da query.
     * @return Query.
     */
    protected <T> TypedQuery<T> getNamedQuery(final String namedQuery,
                                              final Class<T> resultClass, final PageLimits limits,
                                              final Object... params) {
        final TypedQuery<T> typedQuery = getEntityManager()
                .createNamedQuery(namedQuery,
                        resultClass);

        return getQuery(typedQuery, limits, params);
    }

    /**
     * Monta query.
     *
     * @param limits Limites.
     * @param params Parâmetros da query.
     * @return Query.
     */
    protected <T> TypedQuery<T> getQuery(final TypedQuery<T> query,
                                         final PageLimits limits, final Object... params) {
        for (int i = 0; i < params.length; i++) {
            final Object object = params[i];
            query.setParameter(i + 1, object);
        }

        if (limits != null) {
            query.setFirstResult(limits.getOffset());
            query.setMaxResults(limits.getSize());
        }

        return query;
    }

    /**
     * @param clasz       Classe da entidade.
     * @param resultClass Classe do retorno.
     * @param limits      Limites.
     * @param isCount     Se é uma contagem ou uma listagem.
     * @return Query.
     */
    protected <T, E> TypedQuery<E> getQuery(final Class<T> clasz,
                                            final Class<E> resultClass, final PageLimits limits,
                                            final boolean isCount) {
        final EntityManager em = getEntityManager();
        final CriteriaBuilder cb = em.getCriteriaBuilder();
        final CriteriaQuery<E> cq = cb.createQuery(resultClass);
        final Root<T> root = cq.from(clasz);

        // restrições
        if (isCount) {
            // Operação segura: método privado, métodos que chamam devem
            // garantir consistência.
            // Não é possível parametrizar CriteriaQuery.
            final CriteriaQuery cqAux = cq;
            cqAux.select(cb.count(root));
        }

        return getPaginatedQuery(em.createQuery(cq), limits, isCount);
    }

    /**
     * @param typedQuery Query.
     * @param limits     Limites.
     * @param isCount    Se é uma contagem.
     * @return Query.
     */
    protected <E> TypedQuery<E> getPaginatedQuery(
            final TypedQuery<E> typedQuery, final PageLimits limits,
            final boolean isCount) {
        if (!isCount && (limits != null)) {
            typedQuery.setFirstResult(limits.getOffset());
            typedQuery.setMaxResults(limits.getSize());
        }

        return typedQuery;
    }

    @Override
    public void flush() throws Exception {
        getEntityManager().flush();
    }

    @Override
    public Transaction getTransaction() {
        return new JpaTransaction(getEntityManager());
    }

    @Override
    public Cache getCache() {
        return new JpaCache(getEntityManager());
    }
}
